﻿namespace BM.Tools.Xml.Schema
{
    using System.Xml;
    using System.Xml.Schema;
    using System;

    /// <summary>
    /// The visitor class starts with visiting a given <see cref="XmlSchemaObject"/> wraps this object in an object that implements the <see cref="IVisited"/> interface and then recursively traverses the children of this schema object.
    /// </summary>
    /// <typeparam name="TVisited">
    /// the type of the created visited object.
    /// </typeparam>
    public class Visitor<TVisited> where TVisited : class, IVisited
    {
        private XmlNamespaceManager manager;

        private SchemaSetHelper schemaSetHelper;

        /// <summary>
        /// Initializes a new instance of the <see cref="Visitor{TVisited}"/> class.
        /// </summary>
        /// <param name="schemaSet">
        /// The schemaSet.
        /// </param>
        /// <param name="visitedFactory">
        /// The visited factory.
        /// </param>
        public Visitor(XmlSchemaSet schemaSet, VisitedFactory<TVisited> visitedFactory)
        {
            SchemaSet = schemaSet;
            VisitedFactory = visitedFactory;
        }

        /// <summary>
        /// Gets the schema set in which the visiting can be performed.
        /// </summary>
        public XmlSchemaSet SchemaSet
        {
            get;
            private set;
        }

        /// <summary>
        /// Gets the class that is responsible for creating <see cref="TVisited"/> objects.
        /// </summary>
        public VisitedFactory<TVisited> VisitedFactory
        {
            get;
            private set;
        }

        /// <summary>
        /// Gets the xml namespae manager created from the xml schema.
        /// </summary>
        public XmlNamespaceManager Manager
        {
            get
            {
                if (manager == null)
                {
                    EnsureSchemaSetHelperExists();
                }

                return manager;
            }
        }

        /// <summary>
        /// Gets the helper object of the <see cref="SchemaSet"/>.
        /// </summary>
        protected SchemaSetHelper SchemaSetHelper
        {
            get
            {
                EnsureSchemaSetHelperExists();
                return schemaSetHelper;
            }
        }

        /// <summary>
        /// Traverses the xml schema element.
        /// </summary>
        /// <param name="schemaElement">
        /// The schema element.
        /// </param>
        /// <returns>
        /// The root of the visited tree.
        /// </returns>
        public virtual TVisited TraverseElement(XmlSchemaElement schemaElement)
        {
            TVisited result = TraverseAnnotated(schemaElement, null);            
            return result;
        }

        public string RegisterNewPrefix(string uri)
        {
            int index = 1;
            while (Manager.LookupNamespace("p" + index) != null)
            {
                index++;
            }

            Manager.AddNamespace("p" + index, uri);
            return "p" + index;
        }

        protected virtual void PerformCommonForGroupBase(XmlSchemaGroupBase schemaGroupBase, TVisited visited)
        {
            foreach (var schemaObject in schemaGroupBase.Items)
            {
                var schemaAnnotated = schemaObject as XmlSchemaAnnotated;
                if (schemaAnnotated != null)
                {
                    TraverseAnnotated(schemaAnnotated, visited);
                }
            }
        }

        protected virtual TVisited CreateCurrent(TVisited parent, XmlSchemaObject schemaObject)
        {
            return VisitedFactory.Create(parent, schemaObject, this);
        }

        /// <summary>
        /// Checks for cycles in the visited tree.
        /// </summary>
        /// <param name="parent">
        /// The parent.
        /// </param>
        /// <param name="schemaObject">
        /// The schema object.
        /// </param>
        /// <returns>
        /// <c>true</c> if the used schema object was already used
        /// </returns>
        protected virtual bool HasCycles(TVisited parent, XmlSchemaObject schemaObject)
        {
            return parent.IsAncestorOf(schemaObject);
        }

        protected virtual void TraverseAll(XmlSchemaAll xsa, TVisited v)
        {
            PerformCommonForGroupBase(xsa, v);
        }

        /// <summary>
        /// Traverses an annotated xml schema object.
        /// </summary>
        /// <param name="schemaAnnotated">
        /// The annotated xml schema object.
        /// </param>
        /// <param name="parent">
        /// The parent visited object.
        /// </param>
        /// <returns>
        /// The traverse annotated.
        /// </returns>
        protected virtual TVisited TraverseAnnotated(XmlSchemaAnnotated schemaAnnotated, TVisited parent)
        {
            if (schemaAnnotated == null)
            {
                return null;
            }

            var visited = CreateCurrent(parent, schemaAnnotated);
            if (visited == null)
            {
                return null;
            }

            if (schemaAnnotated.Annotation != null)
            {
                TraverseAnnotation(schemaAnnotated.Annotation, visited);
            }

            var fork = new TraverseFork<TVisited>(schemaAnnotated, visited);

            fork.AddTraverseMethod<XmlSchemaAnyAttribute>(TraverseAnyAttribute);
            fork.AddTraverseMethod<XmlSchemaAttribute>(TraverseAttribute);
            fork.AddTraverseMethod<XmlSchemaAttributeGroup>(TraverseAttributeGroup);
            fork.AddTraverseMethod<XmlSchemaAttributeGroupRef>(TraverseAttributeGroupRef);
            fork.AddTraverseMethod<XmlSchemaContentModel>(TraverseContentModel);
            fork.AddTraverseMethod<XmlSchemaContent>(TraverseContent);
            fork.AddTraverseMethod<XmlSchemaFacet>(TraverseFacet);
            fork.AddTraverseMethod<XmlSchemaGroup>(TraverseGroup);
            fork.AddTraverseMethod<XmlSchemaIdentityConstraint>(TraverseIdentityConstraint);
            fork.AddTraverseMethod<XmlSchemaNotation>(TraverseNotation);
            fork.AddTraverseMethod<XmlSchemaParticle>(TraverseParticle);
            fork.AddTraverseMethod<XmlSchemaSimpleTypeContent>(TraverseSimpleTypeContent);
            fork.AddTraverseMethod<XmlSchemaType>(TraverseType);
            fork.AddTraverseMethod<XmlSchemaXPath>(TraverseXPath);

            fork.Run();            

            return visited;
        }

        protected virtual void TraverseAnnotation(XmlSchemaAnnotation xsa, TVisited parent)
        {
            foreach (var item in xsa.Items)
            {
                TVisited visited = CreateCurrent(parent, item);

                var fork = new TraverseFork<TVisited>(xsa, visited);
                fork.AddTraverseMethod<XmlSchemaAppInfo>(TraverseAppInfo);
                fork.AddTraverseMethod<XmlSchemaDocumentation>(TraverseDocumentation);

                fork.Run();                
            }            
        }

        /// <summary>
        /// Traverses the xml schema any object.
        /// </summary>
        /// <param name="schemaAny">
        /// The xml schema any object.
        /// </param>
        /// <param name="visited">
        /// The visited object.
        /// </param>
        protected virtual void TraverseAny(XmlSchemaAny schemaAny, TVisited visited)
        {
        }

        /// <summary>
        /// Traverses the xml schema any attribute object.
        /// </summary>
        /// <param name="schemaAnyAttribute">
        /// The schema any attribute.
        /// </param>
        /// <param name="visited">
        /// The visited object.
        /// </param>
        protected virtual void TraverseAnyAttribute(XmlSchemaAnyAttribute schemaAnyAttribute, TVisited visited)
        {
        }

        /// <summary>
        /// Traverses the schema app info object.
        /// </summary>
        /// <param name="schemaAppInfo">
        /// The schema app info.
        /// </param>
        /// <param name="visited">
        /// The visited object.
        /// </param>
        protected virtual void TraverseAppInfo(XmlSchemaAppInfo schemaAppInfo, TVisited visited)
        {
        }

        protected virtual void TraverseAttribute(XmlSchemaAttribute schemaAttribute, TVisited visited)
        {
            if (!schemaAttribute.RefName.IsEmpty)
            {
                TraverseAnnotated((XmlSchemaAnnotated)SchemaSet.GlobalAttributes[schemaAttribute.RefName], visited);
                return;
            }
            
            if (!schemaAttribute.SchemaTypeName.IsEmpty)
            {
                TraverseTypeName(schemaAttribute.SchemaTypeName, visited);
                return;
            }

            if (schemaAttribute.SchemaType != null)
            {
                TraverseAnnotated(schemaAttribute.SchemaType, visited);
                return;
            }
            
            TraverseBaseType(XmlTypeCode.String, visited);
        }

        protected virtual void TraverseAttributeGroup(XmlSchemaAttributeGroup schemaAttributeGroup, TVisited visited)
        {
            TraverseAttributes(schemaAttributeGroup.Attributes, visited);
        }

        protected virtual void TraverseAttributeGroupRef(XmlSchemaAttributeGroupRef schemaAttributeGroupRef, TVisited visited)
        {
            TraverseAnnotated(SchemaSetHelper.AttributeGroups[schemaAttributeGroupRef.RefName], visited);
        }

        protected virtual void TraverseAttributes(XmlSchemaObjectCollection schemaObjectCollection, TVisited visited)
        {
            foreach (XmlSchemaAttribute schemaAttribute in schemaObjectCollection)
            {
                TraverseAnnotated(schemaAttribute, visited);
            }
        }

        protected virtual void TraverseBaseType(XmlTypeCode typeCode, TVisited visited)
        {
        }

        protected virtual void TraverseChoice(XmlSchemaChoice schemaChoice, TVisited visited)
        {
            PerformCommonForGroupBase(schemaChoice, visited);
        }

        protected virtual void TraverseComplexContent(XmlSchemaComplexContent schemaComplexContent, TVisited visited)
        {
            TraverseAnnotated(schemaComplexContent.Content, visited);
        }

        protected virtual void TraverseComplexContentExtension(XmlSchemaComplexContentExtension schemaComplexContentExtension, TVisited visited)
        {
            TraverseTypeName(schemaComplexContentExtension.BaseTypeName, visited);
            TraverseAttributes(schemaComplexContentExtension.Attributes, visited);
            TraverseAnnotated(schemaComplexContentExtension.Particle, visited);
        }

        protected virtual void TraverseComplexContentRestriction(XmlSchemaComplexContentRestriction schemaComplexContentRestriction, TVisited visited)
        {
            TraverseTypeName(schemaComplexContentRestriction.BaseTypeName, visited);
            TraverseAttributes(schemaComplexContentRestriction.Attributes, visited);
            TraverseAnnotated(schemaComplexContentRestriction.Particle, visited);
        }

        protected virtual void TraverseComplexType(XmlSchemaComplexType schemaComplexType, TVisited parent)
        {
            TraverseAttributes(schemaComplexType.Attributes, parent);
            TraverseAnnotated(schemaComplexType.Particle, parent);
            TraverseAnnotated(schemaComplexType.ContentModel, parent);
        }

        protected virtual void TraverseConstraints(XmlSchemaObjectCollection schemaObjectCollection, TVisited visited)
        {
            foreach (XmlSchemaIdentityConstraint identityConstraint in schemaObjectCollection)
            {
                TraverseAnnotated(identityConstraint, visited);
            }
        }

        protected virtual void TraverseContent(XmlSchemaContent schemaContent, TVisited visited)
        {
            var fork = new TraverseFork<TVisited>(schemaContent, visited);
            fork.AddTraverseMethod<XmlSchemaSimpleContentExtension>(TraverseSimpleContentExtension);
            fork.AddTraverseMethod<XmlSchemaSimpleContentRestriction>(TraverseSimpleContentRestriction);
            fork.AddTraverseMethod<XmlSchemaComplexContentExtension>(TraverseComplexContentExtension);
            fork.AddTraverseMethod<XmlSchemaComplexContentRestriction>(TraverseComplexContentRestriction);
            fork.Run();
        }

        protected virtual void TraverseContentModel(XmlSchemaContentModel schemaContentModel, TVisited visited)
        {
            var fork = new TraverseFork<TVisited>(schemaContentModel, visited);
            fork.AddTraverseMethod<XmlSchemaComplexContent>(TraverseComplexContent);
            fork.AddTraverseMethod<XmlSchemaSimpleContent>(TraverseSimpleContent);
            
            fork.Run();
        }

        protected virtual void TraverseCycle(XmlSchemaElement xse, TVisited v)
        {
        }

        protected virtual void TraverseDocumentation(XmlSchemaDocumentation x, TVisited v)
        {
        }

        protected virtual void TraverseElement(XmlSchemaElement schemaElement, TVisited v)
        {
            if (HasCycles(v, schemaElement))
            {
                TraverseCycle(schemaElement, v);
                return;
            }

            var complexType = schemaElement.ElementSchemaType as XmlSchemaComplexType;
            if ((complexType != null && complexType.IsAbstract) || schemaElement.IsAbstract)
            {
                TraverseSubstitutions(schemaElement, v);
                return;
            }

            if (!schemaElement.RefName.IsEmpty)
            {
                TraverseAnnotated((XmlSchemaElement)SchemaSet.GlobalElements[schemaElement.RefName], v);
            }

            if (!schemaElement.SchemaTypeName.IsEmpty)
            {
                TraverseTypeName(schemaElement.SchemaTypeName, v);
            }

            if (schemaElement.Constraints.Count != 0)
            {
                TraverseConstraints(schemaElement.Constraints, v);
            }

            if (schemaElement.SchemaType != null)
            {
                TraverseAnnotated(schemaElement.SchemaType, v);
            }
        }

        protected virtual void TraverseEnumerationFacet(XmlSchemaEnumerationFacet schemaEnumerationFacet, TVisited visited)
        {
        }

        protected virtual void TraverseFacet(XmlSchemaFacet schemaFacet, TVisited visited)
        {
            var fork = new TraverseFork<TVisited>(schemaFacet, visited);

            fork.AddTraverseMethod<XmlSchemaEnumerationFacet>(TraverseEnumerationFacet);
            fork.AddTraverseMethod<XmlSchemaNumericFacet>(TraverseNumericFacet);
            fork.AddTraverseMethod<XmlSchemaMaxExclusiveFacet>(TraverseMaxExclusiveFacet);
            fork.AddTraverseMethod<XmlSchemaMaxInclusiveFacet>(TraverseMaxInclusiveFacet);
            fork.AddTraverseMethod<XmlSchemaMinExclusiveFacet>(TraverseMinExclusiveFacet);
            fork.AddTraverseMethod<XmlSchemaMinInclusiveFacet>(TraverseMinInclusiveFacet);
            fork.AddTraverseMethod<XmlSchemaPatternFacet>(TraversePatternFacet);
            fork.AddTraverseMethod<XmlSchemaWhiteSpaceFacet>(TraverseWhiteSpaceFacet);

            fork.Run();            
        }

        protected virtual void TraverseFacets(XmlSchemaObjectCollection schemaObjectCollection, TVisited visited)
        {
            foreach (XmlSchemaFacet facet in schemaObjectCollection)
            {
                TraverseAnnotated(facet, visited);
            }
        }

        protected virtual void TraverseFractionDigitsFacet(XmlSchemaFractionDigitsFacet x, TVisited v)
        {
        }

        protected virtual void TraverseGroup(XmlSchemaGroup xsg, TVisited v)
        {
            TraverseAnnotated(xsg.Particle, v);
        }

        protected virtual void TraverseGroupBase(XmlSchemaGroupBase xsgb, TVisited visited)
        {
            var fork = new TraverseFork<TVisited>(xsgb, visited);
            fork.AddTraverseMethod<XmlSchemaAll>(TraverseAll);
            fork.AddTraverseMethod<XmlSchemaSequence>(TraverseSequence);
            fork.AddTraverseMethod<XmlSchemaChoice>(TraverseChoice);

            fork.Run();
        }

        protected virtual void TraverseGroupRef(XmlSchemaGroupRef xsgr, TVisited v)
        {
            TraverseAnnotated(SchemaSetHelper.Groups[xsgr.RefName], v);
        }

        protected virtual void TraverseIdentityConstraint(XmlSchemaIdentityConstraint schemaIdentityConstraint, TVisited visited)
        {
            var fork = new TraverseFork<TVisited>(schemaIdentityConstraint, visited);
            fork.AddTraverseMethod<XmlSchemaKey>(TraverseKey);
            fork.AddTraverseMethod<XmlSchemaKeyref>(TraverseKeyref);
            fork.AddTraverseMethod<XmlSchemaUnique>(TraverseUnique);

            fork.Run();
        }

        protected virtual void TraverseKey(XmlSchemaKey schemaKey, TVisited visited)
        {
        }

        protected virtual void TraverseKeyref(XmlSchemaKeyref schemaKeyref, TVisited visited)
        {
        }

        protected virtual void TraverseLengthFacet(XmlSchemaLengthFacet schemaLengthFacet, TVisited visited)
        {
        }

        protected virtual void TraverseMaxExclusiveFacet(XmlSchemaMaxExclusiveFacet schemaMaxExclusiveFacet, TVisited visited)
        {
        }

        protected virtual void TraverseMaxInclusiveFacet(XmlSchemaMaxInclusiveFacet schemaMaxInclusiveFacet, TVisited visited)
        {
        }

        protected virtual void TraverseMaxLengthFacet(XmlSchemaMaxLengthFacet schemaMaxLengthFacet, TVisited visited)
        {
        }

        protected virtual void TraverseMinExclusiveFacet(XmlSchemaMinExclusiveFacet schemaMinExclusiveFacet, TVisited visited)
        {
        }

        protected virtual void TraverseMinInclusiveFacet(XmlSchemaMinInclusiveFacet schemaMinInclusiveFacet, TVisited visited)
        {
        }

        protected virtual void TraverseMinLengthFacet(XmlSchemaMinLengthFacet schemaMinLengthFacet, TVisited visited)
        {
        }

        protected virtual void TraverseNotation(XmlSchemaNotation schemaNotation, TVisited visited)
        {
        }

        protected virtual void TraverseNumericFacet(XmlSchemaNumericFacet schemaNumericFacet, TVisited visited)
        {
            var fork = new TraverseFork<TVisited>(schemaNumericFacet, visited);
            fork.AddTraverseMethod<XmlSchemaFractionDigitsFacet>(TraverseFractionDigitsFacet);
            fork.AddTraverseMethod<XmlSchemaLengthFacet>(TraverseLengthFacet);
            fork.AddTraverseMethod<XmlSchemaMinLengthFacet>(TraverseMinLengthFacet);            
            fork.AddTraverseMethod<XmlSchemaMaxLengthFacet>(TraverseMaxLengthFacet);
            fork.AddTraverseMethod<XmlSchemaTotalDigitsFacet>(TraverseTotalDigitsFacet);

            fork.Run();    
        }        

        protected virtual void TraverseParticle(XmlSchemaParticle schemaParticle, TVisited visited)
        {
            var fork = new TraverseFork<TVisited>(schemaParticle, visited);
            fork.AddTraverseMethod<XmlSchemaElement>(TraverseElement);
            fork.AddTraverseMethod<XmlSchemaGroupBase>(TraverseGroupBase);
            fork.AddTraverseMethod<XmlSchemaGroupRef>(TraverseGroupRef);
            fork.AddTraverseMethod<XmlSchemaAny>(TraverseAny);

            fork.Run();                    
        }

        protected virtual void TraversePatternFacet(XmlSchemaPatternFacet schemaPatternFacet, TVisited visited)
        {
        }

        protected virtual void TraverseSequence(XmlSchemaSequence schemaSequence, TVisited visited)
        {
            PerformCommonForGroupBase(schemaSequence, visited);
        }

        protected virtual void TraverseSimpleContent(XmlSchemaSimpleContent schemaSimpleContent, TVisited visited)
        {
            TraverseAnnotated(schemaSimpleContent.Content, visited);
        }

        protected virtual void TraverseSimpleContentExtension(XmlSchemaSimpleContentExtension schemaSimpleContentExtension, TVisited visited)
        {
            TraverseTypeName(schemaSimpleContentExtension.BaseTypeName, visited);
            TraverseAttributes(schemaSimpleContentExtension.Attributes, visited);
        }

        protected virtual void TraverseSimpleContentRestriction(XmlSchemaSimpleContentRestriction schemaSimpleContentRestriction, TVisited visited)
        {
            TraverseTypeName(schemaSimpleContentRestriction.BaseTypeName, visited);
            TraverseFacets(schemaSimpleContentRestriction.Facets, visited);
            TraverseAttributes(schemaSimpleContentRestriction.Attributes, visited);
        }

        protected virtual void TraverseSimpleType(XmlSchemaSimpleType schemaSimpleType, TVisited parent)
        {
            if (schemaSimpleType.Content != null)
            {
                TraverseAnnotated(schemaSimpleType.Content, parent);
            }
        }

        protected virtual void TraverseSimpleTypeContent(XmlSchemaSimpleTypeContent schemaSimpleTypeContent, TVisited visited)
        {
            var fork = new TraverseFork<TVisited>(schemaSimpleTypeContent, visited);
            fork.AddTraverseMethod<XmlSchemaSimpleTypeList>(TraverseSimpleTypeList);
            fork.AddTraverseMethod<XmlSchemaSimpleTypeRestriction>(TraverseSimpleTypeRestriction);
            fork.AddTraverseMethod<XmlSchemaSimpleTypeUnion>(TraverseSimpleTypeUnion);

            fork.Run();    
        }

        protected virtual void TraverseSimpleTypeList(XmlSchemaSimpleTypeList schemaSimpleTypeList, TVisited visited)
        {
        }

        protected virtual void TraverseSimpleTypeRestriction(XmlSchemaSimpleTypeRestriction schemaSimpleTypeRestriction, TVisited visited)
        {
            TraverseTypeName(schemaSimpleTypeRestriction.BaseTypeName, visited);
            TraverseFacets(schemaSimpleTypeRestriction.Facets, visited);
        }

        protected virtual void TraverseSimpleTypeUnion(XmlSchemaSimpleTypeUnion schemaSimpleTypeUnion, TVisited visited)
        {
        }

        protected virtual void TraverseSubstitutions(XmlSchemaElement schemaElement, TVisited visited)
        {
            foreach (var substitutionElement in SchemaSetHelper.GetSubstitutions(schemaElement))
            {
                TraverseAnnotated(substitutionElement, visited);
            }
        }

        protected virtual void TraverseTotalDigitsFacet(XmlSchemaTotalDigitsFacet schemaTotalDigitsFacet, TVisited visited)
        {
        }

        protected virtual void TraverseType(XmlSchemaType schemaType, TVisited visited)
        {
            var fork = new TraverseFork<TVisited>(schemaType, visited);
            fork.AddTraverseMethod<XmlSchemaComplexType>(TraverseComplexType);
            fork.AddTraverseMethod<XmlSchemaSimpleType>(TraverseSimpleType);            

            fork.Run();            
        }

        protected virtual void TraverseTypeName(XmlQualifiedName qualifiedName, TVisited visited)
        {
            var baseType = GetTypeCode(qualifiedName);
            if (baseType != null)
            {
                TraverseBaseType(baseType.Value, visited);
            }

            TraverseAnnotated((XmlSchemaType)SchemaSet.GlobalTypes[qualifiedName], visited);
        }

        public static XmlTypeCode? GetTypeCode(XmlQualifiedName qualifiedName)
        {
            if (!qualifiedName.Namespace.Equals("http://www.w3.org/2001/XMLSchema"))
            {
                return null;
            }

            if (qualifiedName.Name == "anySimpleType")
            {
                return XmlTypeCode.UntypedAtomic;
            }

            if (qualifiedName.Name == "anyType")
            {
                return XmlTypeCode.Item;
            }

            try
            {
                return (XmlTypeCode)Enum.Parse(typeof(XmlTypeCode), qualifiedName.Name, true);
            }
            catch (ArgumentException)
            {
                return null;
            }
        }

        protected virtual void TraverseUnique(XmlSchemaUnique schemaUnique, TVisited visited)
        {
        }

        protected virtual void TraverseWhiteSpaceFacet(XmlSchemaWhiteSpaceFacet schemaWhiteSpaceFacet, TVisited visited)
        {
        }

        protected virtual void TraverseXPath(XmlSchemaXPath schemaXPath, TVisited visited)
        {
        }

        private void EnsureSchemaSetHelperExists()
        {
            if (schemaSetHelper == null)
            {
                schemaSetHelper = new SchemaSetHelper(SchemaSet);
                manager = schemaSetHelper.GetManager();
            }
        }      
    }
}
